/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package java.util;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * {@code GregorianCalendar} is a concrete subclass of {@link Calendar}
 * and provides the standard calendar used by most of the world.
 * <p>
 * <p>
 * The standard (Gregorian) calendar has 2 eras, BC and AD.
 * <p>
 * <p>
 * This implementation handles a single discontinuity, which corresponds by
 * default to the date the Gregorian calendar was instituted (October 15, 1582
 * in some countries, later in others). The cutover date may be changed by the
 * caller by calling {@code setGregorianChange()}.
 * <p>
 * <p>
 * Historically, in those countries which adopted the Gregorian calendar first,
 * October 4, 1582 was thus followed by October 15, 1582. This calendar models
 * this correctly. Before the Gregorian cutover, {@code GregorianCalendar}
 * implements the Julian calendar. The only difference between the Gregorian and
 * the Julian calendar is the leap year rule. The Julian calendar specifies leap
 * years every four years, whereas the Gregorian calendar omits century years
 * which are not divisible by 400.
 * <p>
 * <p>
 * {@code GregorianCalendar} implements <em>proleptic</em> Gregorian
 * and Julian calendars. That is, dates are computed by extrapolating the
 * current rules indefinitely far backward and forward in time. As a result,
 * {@code GregorianCalendar} may be used for all years to generate
 * meaningful and consistent results. However, dates obtained using
 * {@code GregorianCalendar} are historically accurate only from March 1,
 * 4 AD onward, when modern Julian calendar rules were adopted. Before this
 * date, leap year rules were applied irregularly, and before 45 BC the Julian
 * calendar did not even exist.
 * <p>
 * <p>
 * Prior to the institution of the Gregorian calendar, New Year's Day was March
 * 25. To avoid confusion, this calendar always uses January 1. A manual
 * adjustment may be made if desired for dates that are prior to the Gregorian
 * changeover and which fall between January 1 and March 24.
 * <p>
 * <p>
 * Values calculated for the {@code WEEK_OF_YEAR} field range from 1 to
 * 53. Week 1 for a year is the earliest seven day period starting on
 * {@code getFirstDayOfWeek()} that contains at least
 * {@code getMinimalDaysInFirstWeek()} days from that year. It thus
 * depends on the values of {@code getMinimalDaysInFirstWeek()},
 * {@code getFirstDayOfWeek()}, and the day of the week of January 1.
 * Weeks between week 1 of one year and week 1 of the following year are
 * numbered sequentially from 2 to 52 or 53 (as needed).
 * <p>
 * <p>
 * For example, January 1, 1998 was a Thursday. If
 * {@code getFirstDayOfWeek()} is {@code MONDAY} and
 * {@code getMinimalDaysInFirstWeek()} is 4 (these are the values
 * reflecting ISO 8601 and many national standards), then week 1 of 1998 starts
 * on December 29, 1997, and ends on January 4, 1998. If, however,
 * {@code getFirstDayOfWeek()} is {@code SUNDAY}, then week 1 of
 * 1998 starts on January 4, 1998, and ends on January 10, 1998; the first three
 * days of 1998 then are part of week 53 of 1997.
 * <p>
 * <p>
 * Values calculated for the {@code WEEK_OF_MONTH} field range from 0 or
 * 1 to 4 or 5. Week 1 of a month (the days with <code>WEEK_OF_MONTH =
 * 1</code>)
 * is the earliest set of at least {@code getMinimalDaysInFirstWeek()}
 * contiguous days in that month, ending on the day before
 * {@code getFirstDayOfWeek()}. Unlike week 1 of a year, week 1 of a
 * month may be shorter than 7 days, need not start on
 * {@code getFirstDayOfWeek()}, and will not include days of the
 * previous month. Days of a month before week 1 have a
 * {@code WEEK_OF_MONTH} of 0.
 * <p>
 * <p>
 * For example, if {@code getFirstDayOfWeek()} is {@code SUNDAY}
 * and {@code getMinimalDaysInFirstWeek()} is 4, then the first week of
 * January 1998 is Sunday, January 4 through Saturday, January 10. These days
 * have a {@code WEEK_OF_MONTH} of 1. Thursday, January 1 through
 * Saturday, January 3 have a {@code WEEK_OF_MONTH} of 0. If
 * {@code getMinimalDaysInFirstWeek()} is changed to 3, then January 1
 * through January 3 have a {@code WEEK_OF_MONTH} of 1.
 * <p>
 * <p>
 * <strong>Example:</strong> <blockquote>
 * <p>
 * <pre>
 * // get the supported ids for GMT-08:00 (Pacific Standard Time)
 * String[] ids = TimeZone.getAvailableIDs(-8 * 60 * 60 * 1000);
 * // if no ids were returned, something is wrong. get out.
 * if (ids.length == 0)
 *     System.exit(0);
 *
 *  // begin output
 * System.out.println("Current Time");
 *
 * // create a Pacific Standard Time time zone
 * SimpleTimeZone pdt = new SimpleTimeZone(-8 * 60 * 60 * 1000, ids[0]);
 *
 * // set up rules for daylight savings time
 * pdt.setStartRule(Calendar.APRIL, 1, Calendar.SUNDAY, 2 * 60 * 60 * 1000);
 * pdt.setEndRule(Calendar.OCTOBER, -1, Calendar.SUNDAY, 2 * 60 * 60 * 1000);
 *
 * // create a GregorianCalendar with the Pacific Daylight time zone
 * // and the current date and time
 * Calendar calendar = new GregorianCalendar(pdt);
 * Date trialTime = new Date();
 * calendar.setTime(trialTime);
 *
 * // print out a bunch of interesting things
 * System.out.println("ERA: " + calendar.get(Calendar.ERA));
 * System.out.println("YEAR: " + calendar.get(Calendar.YEAR));
 * System.out.println("MONTH: " + calendar.get(Calendar.MONTH));
 * System.out.println("WEEK_OF_YEAR: " + calendar.get(Calendar.WEEK_OF_YEAR));
 * System.out.println("WEEK_OF_MONTH: " + calendar.get(Calendar.WEEK_OF_MONTH));
 * System.out.println("DATE: " + calendar.get(Calendar.DATE));
 * System.out.println("DAY_OF_MONTH: " + calendar.get(Calendar.DAY_OF_MONTH));
 * System.out.println("DAY_OF_YEAR: " + calendar.get(Calendar.DAY_OF_YEAR));
 * System.out.println("DAY_OF_WEEK: " + calendar.get(Calendar.DAY_OF_WEEK));
 * System.out.println("DAY_OF_WEEK_IN_MONTH: "
 *                    + calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH));
 * System.out.println("AM_PM: " + calendar.get(Calendar.AM_PM));
 * System.out.println("HOUR: " + calendar.get(Calendar.HOUR));
 * System.out.println("HOUR_OF_DAY: " + calendar.get(Calendar.HOUR_OF_DAY));
 * System.out.println("MINUTE: " + calendar.get(Calendar.MINUTE));
 * System.out.println("SECOND: " + calendar.get(Calendar.SECOND));
 * System.out.println("MILLISECOND: " + calendar.get(Calendar.MILLISECOND));
 * System.out.println("ZONE_OFFSET: "
 *                    + (calendar.get(Calendar.ZONE_OFFSET)/(60*60*1000)));
 * System.out.println("DST_OFFSET: "
 *                    + (calendar.get(Calendar.DST_OFFSET)/(60*60*1000)));
 *
 * System.out.println("Current Time, with hour reset to 3");
 * calendar.clear(Calendar.HOUR_OF_DAY); // so doesn't override
 * calendar.set(Calendar.HOUR, 3);
 * System.out.println("ERA: " + calendar.get(Calendar.ERA));
 * System.out.println("YEAR: " + calendar.get(Calendar.YEAR));
 * System.out.println("MONTH: " + calendar.get(Calendar.MONTH));
 * System.out.println("WEEK_OF_YEAR: " + calendar.get(Calendar.WEEK_OF_YEAR));
 * System.out.println("WEEK_OF_MONTH: " + calendar.get(Calendar.WEEK_OF_MONTH));
 * System.out.println("DATE: " + calendar.get(Calendar.DATE));
 * System.out.println("DAY_OF_MONTH: " + calendar.get(Calendar.DAY_OF_MONTH));
 * System.out.println("DAY_OF_YEAR: " + calendar.get(Calendar.DAY_OF_YEAR));
 * System.out.println("DAY_OF_WEEK: " + calendar.get(Calendar.DAY_OF_WEEK));
 * System.out.println("DAY_OF_WEEK_IN_MONTH: "
 *                    + calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH));
 * System.out.println("AM_PM: " + calendar.get(Calendar.AM_PM));
 * System.out.println("HOUR: " + calendar.get(Calendar.HOUR));
 * System.out.println("HOUR_OF_DAY: " + calendar.get(Calendar.HOUR_OF_DAY));
 * System.out.println("MINUTE: " + calendar.get(Calendar.MINUTE));
 * System.out.println("SECOND: " + calendar.get(Calendar.SECOND));
 * System.out.println("MILLISECOND: " + calendar.get(Calendar.MILLISECOND));
 * System.out.println("ZONE_OFFSET: "
 *        + (calendar.get(Calendar.ZONE_OFFSET)/(60*60*1000))); // in hours
 * System.out.println("DST_OFFSET: "
 *        + (calendar.get(Calendar.DST_OFFSET)/(60*60*1000))); // in hours
 * </pre>
 * <p>
 * </blockquote>
 *
 * @see Calendar
 * @see TimeZone
 */
public class GregorianCalendar extends Calendar {
	/**
	 * Value for the BC era.
	 */
	public static final int BC = 0;

	/**
	 * Value for the AD era.
	 */
	public static final int AD = 1;

	private static final long defaultGregorianCutover = -12219292800000l;

	private long gregorianCutover = defaultGregorianCutover;

	private transient int changeYear = 1582;

	private transient int julianSkew = ((changeYear - 2000) / 400)
		+ julianError() - ((changeYear - 2000) / 100);

	static byte[] DaysInMonth = new byte[]{31, 28, 31, 30, 31, 30, 31, 31,
		30, 31, 30, 31};

	private static int[] DaysInYear = new int[]{0, 31, 59, 90, 120, 151, 181,
		212, 243, 273, 304, 334};

	private static int[] maximums = new int[]{1, 292278994, 11, 53, 6, 31,
		366, 7, 6, 1, 11, 23, 59, 59, 999, 14 * 3600 * 1000, 7200000};

	private static int[] minimums = new int[]{0, 1, 0, 1, 0, 1, 1, 1, 1, 0,
		0, 0, 0, 0, 0, -13 * 3600 * 1000, 0};

	private static int[] leastMaximums = new int[]{1, 292269054, 11, 50, 3,
		28, 355, 7, 3, 1, 11, 23, 59, 59, 999, 50400000, 1200000};

	private int currentYearSkew = 10;

	private int lastYearSkew = 0;

	/**
	 * Constructs a new {@code GregorianCalendar} initialized to the current date and
	 * time with the default {@code Locale} and {@code TimeZone}.
	 */
	public GregorianCalendar() {
		this(TimeZone.getDefault(), Locale.getDefault());
	}

	/**
	 * Constructs a new {@code GregorianCalendar} initialized to midnight in the default
	 * {@code TimeZone} and {@code Locale} on the specified date.
	 *
	 * @param year  the year.
	 * @param month the month.
	 * @param day   the day of the month.
	 */
	public GregorianCalendar(int year, int month, int day) {
		super(TimeZone.getDefault(), Locale.getDefault());
		set(year, month, day);
	}

	/**
	 * Constructs a new {@code GregorianCalendar} initialized to the specified date and
	 * time in the default {@code TimeZone} and {@code Locale}.
	 *
	 * @param year   the year.
	 * @param month  the month.
	 * @param day    the day of the month.
	 * @param hour   the hour.
	 * @param minute the minute.
	 */
	public GregorianCalendar(int year, int month, int day, int hour, int minute) {
		super(TimeZone.getDefault(), Locale.getDefault());
		set(year, month, day, hour, minute);
	}

	/**
	 * Constructs a new {@code GregorianCalendar} initialized to the specified date and
	 * time in the default {@code TimeZone} and {@code Locale}.
	 *
	 * @param year   the year.
	 * @param month  the month.
	 * @param day    the day of the month.
	 * @param hour   the hour.
	 * @param minute the minute.
	 * @param second the second.
	 */
	public GregorianCalendar(int year, int month, int day, int hour,
							 int minute, int second) {
		super(TimeZone.getDefault(), Locale.getDefault());
		set(year, month, day, hour, minute, second);
	}

	GregorianCalendar(long milliseconds) {
		this(false);
		setTimeInMillis(milliseconds);
	}

	/**
	 * Constructs a new {@code GregorianCalendar} initialized to the current date and
	 * time and using the specified {@code Locale} and the default {@code TimeZone}.
	 *
	 * @param locale the {@code Locale}.
	 */
	public GregorianCalendar(Locale locale) {
		this(TimeZone.getDefault(), locale);
	}

	/**
	 * Constructs a new {@code GregorianCalendar} initialized to the current date and
	 * time and using the specified {@code TimeZone} and the default {@code Locale}.
	 *
	 * @param timezone the {@code TimeZone}.
	 */
	public GregorianCalendar(TimeZone timezone) {
		this(timezone, Locale.getDefault());
	}

	/**
	 * Constructs a new {@code GregorianCalendar} initialized to the current date and
	 * time and using the specified {@code TimeZone} and {@code Locale}.
	 *
	 * @param timezone the {@code TimeZone}.
	 * @param locale   the {@code Locale}.
	 */
	public GregorianCalendar(TimeZone timezone, Locale locale) {
		super(timezone, locale);
		setTimeInMillis(System.currentTimeMillis());
	}

	GregorianCalendar(boolean ignored) {
		super(TimeZone.getDefault());
		setFirstDayOfWeek(SUNDAY);
		setMinimalDaysInFirstWeek(1);
	}

	/**
	 * Adds the specified amount to a {@code Calendar} field.
	 *
	 * @param field the {@code Calendar} field to modify.
	 * @param value the amount to add to the field.
	 * @throws IllegalArgumentException if the specified field is DST_OFFSET or ZONE_OFFSET.
	 */
	@Override
	public void add(int field, int value) {
		if (value == 0) {
			return;
		}
		if (field < 0 || field >= ZONE_OFFSET) {
			throw new IllegalArgumentException();
		}

		if (field == ERA) {
			complete();
			if (fields[ERA] == AD) {
				if (value >= 0) {
					return;
				}
				set(ERA, BC);
			} else {
				if (value <= 0) {
					return;
				}
				set(ERA, AD);
			}
			complete();
			return;
		}

		if (field == YEAR || field == MONTH) {
			complete();
			if (field == MONTH) {
				int month = fields[MONTH] + value;
				if (month < 0) {
					value = (month - 11) / 12;
					month = 12 + (month % 12);
				} else {
					value = month / 12;
				}
				set(MONTH, month % 12);
			}
			set(YEAR, fields[YEAR] + value);
			int days = daysInMonth(isLeapYear(fields[YEAR]), fields[MONTH]);
			if (fields[DATE] > days) {
				set(DATE, days);
			}
			complete();
			return;
		}

		long multiplier = 0;
		getTimeInMillis(); // Update the time
		switch (field) {
			case MILLISECOND:
				time += value;
				break;
			case SECOND:
				time += value * 1000L;
				break;
			case MINUTE:
				time += value * 60000L;
				break;
			case HOUR:
			case HOUR_OF_DAY:
				time += value * 3600000L;
				break;
			case AM_PM:
				multiplier = 43200000L;
				break;
			case DATE:
			case DAY_OF_YEAR:
			case DAY_OF_WEEK:
				multiplier = 86400000L;
				break;
			case WEEK_OF_YEAR:
			case WEEK_OF_MONTH:
			case DAY_OF_WEEK_IN_MONTH:
				multiplier = 604800000L;
				break;
		}

		if (multiplier == 0) {
			areFieldsSet = false;
			complete();
			return;
		}

		long delta = value * multiplier;

        /*
		 * Attempt to keep the hour and minute constant when we've crossed a DST
         * boundary and the user's units are AM_PM or larger. The typical
         * consequence is that calls to add(DATE, 1) will add 23, 24 or 25 hours
         * depending on whether the DST goes forward, constant, or backward.
         *
         * We know we've crossed a DST boundary if the new time will have a
         * different timezone offset. Adjust by adding the difference of the two
         * offsets. We don't adjust when doing so prevents the change from
         * crossing the boundary.
         */
		int zoneOffset = getTimeZone().getRawOffset();
		int offsetBefore = getOffset(time + zoneOffset);
		int offsetAfter = getOffset(time + zoneOffset + delta);
		int dstDelta = offsetBefore - offsetAfter;
		if (getOffset(time + zoneOffset + delta + dstDelta) == offsetAfter) {
			delta += dstDelta;
		}

		time += delta;
		areFieldsSet = false;
		complete();
	}

	private void fullFieldsCalc(long timeVal, int zoneOffset) {
		int millis = (int) (time % 86400000);
		long days = timeVal / 86400000;

		if (millis < 0) {
			millis += 86400000;
			days--;
		}
		// Cannot add ZONE_OFFSET to time as it might overflow
		millis += zoneOffset;
		while (millis < 0) {
			millis += 86400000;
			days--;
		}
		while (millis >= 86400000) {
			millis -= 86400000;
			days++;
		}

		int dayOfYear = computeYearAndDay(days, timeVal + zoneOffset);
		fields[DAY_OF_YEAR] = dayOfYear;
		if (fields[YEAR] == changeYear && gregorianCutover <= timeVal + zoneOffset) {
			dayOfYear += currentYearSkew;
		}
		int month = dayOfYear / 32;
		boolean leapYear = isLeapYear(fields[YEAR]);
		int date = dayOfYear - daysInYear(leapYear, month);
		if (date > daysInMonth(leapYear, month)) {
			date -= daysInMonth(leapYear, month);
			month++;
		}
		fields[DAY_OF_WEEK] = mod7(days - 3) + 1;
		int dstOffset = fields[YEAR] <= 0 ? 0 : getTimeZone().getOffset(AD,
			fields[YEAR], month, date, fields[DAY_OF_WEEK], millis);
		if (fields[YEAR] > 0) {
			dstOffset -= zoneOffset;
		}
		fields[DST_OFFSET] = dstOffset;
		if (dstOffset != 0) {
			long oldDays = days;
			millis += dstOffset;
			if (millis < 0) {
				millis += 86400000;
				days--;
			} else if (millis >= 86400000) {
				millis -= 86400000;
				days++;
			}
			if (oldDays != days) {
				dayOfYear = computeYearAndDay(days, timeVal - zoneOffset
					+ dstOffset);
				fields[DAY_OF_YEAR] = dayOfYear;
				if (fields[YEAR] == changeYear && gregorianCutover <= timeVal - zoneOffset + dstOffset) {
					dayOfYear += currentYearSkew;
				}
				month = dayOfYear / 32;
				leapYear = isLeapYear(fields[YEAR]);
				date = dayOfYear - daysInYear(leapYear, month);
				if (date > daysInMonth(leapYear, month)) {
					date -= daysInMonth(leapYear, month);
					month++;
				}
				fields[DAY_OF_WEEK] = mod7(days - 3) + 1;
			}
		}

		fields[MILLISECOND] = (millis % 1000);
		millis /= 1000;
		fields[SECOND] = (millis % 60);
		millis /= 60;
		fields[MINUTE] = (millis % 60);
		millis /= 60;
		fields[HOUR_OF_DAY] = (millis % 24);
		fields[AM_PM] = fields[HOUR_OF_DAY] > 11 ? 1 : 0;
		fields[HOUR] = fields[HOUR_OF_DAY] % 12;

		if (fields[YEAR] <= 0) {
			fields[ERA] = BC;
			fields[YEAR] = -fields[YEAR] + 1;
		} else {
			fields[ERA] = AD;
		}
		fields[MONTH] = month;
		fields[DATE] = date;
		fields[DAY_OF_WEEK_IN_MONTH] = (date - 1) / 7 + 1;
		fields[WEEK_OF_MONTH] = (date - 1 + mod7(days - date - 2
			- (getFirstDayOfWeek() - 1))) / 7 + 1;
		int daysFromStart = mod7(days - 3 - (fields[DAY_OF_YEAR] - 1)
			- (getFirstDayOfWeek() - 1));
		int week = (fields[DAY_OF_YEAR] - 1 + daysFromStart) / 7
			+ (7 - daysFromStart >= getMinimalDaysInFirstWeek() ? 1 : 0);
		if (week == 0) {
			fields[WEEK_OF_YEAR] = 7 - mod7(daysFromStart
				- (isLeapYear(fields[YEAR] - 1) ? 2 : 1)) >= getMinimalDaysInFirstWeek() ? 53
				: 52;
		} else if (fields[DAY_OF_YEAR] >= (leapYear ? 367 : 366)
			- mod7(daysFromStart + (leapYear ? 2 : 1))) {
			fields[WEEK_OF_YEAR] = 7 - mod7(daysFromStart + (leapYear ? 2 : 1)) >= getMinimalDaysInFirstWeek() ? 1
				: week;
		} else {
			fields[WEEK_OF_YEAR] = week;
		}
	}

	@Override
	protected void computeFields() {
		TimeZone timeZone = getTimeZone();
		int dstOffset = timeZone.inDaylightTime(new Date(time)) ? timeZone.getDSTSavings() : 0;
		int zoneOffset = timeZone.getRawOffset();
		fields[DST_OFFSET] = dstOffset;
		fields[ZONE_OFFSET] = zoneOffset;

		fullFieldsCalc(time, zoneOffset);

		for (int i = 0; i < FIELD_COUNT; i++) {
			isSet[i] = true;
		}
	}

	@Override
	protected void computeTime() {
		if (!isLenient()) {
			if (isSet[HOUR_OF_DAY]) {
				if (fields[HOUR_OF_DAY] < 0 || fields[HOUR_OF_DAY] > 23) {
					throw new IllegalArgumentException();
				}
			} else if (isSet[HOUR] && (fields[HOUR] < 0 || fields[HOUR] > 11)) {
				throw new IllegalArgumentException();
			}
			if (isSet[MINUTE] && (fields[MINUTE] < 0 || fields[MINUTE] > 59)) {
				throw new IllegalArgumentException();
			}
			if (isSet[SECOND] && (fields[SECOND] < 0 || fields[SECOND] > 59)) {
				throw new IllegalArgumentException();
			}
			if (isSet[MILLISECOND]
				&& (fields[MILLISECOND] < 0 || fields[MILLISECOND] > 999)) {
				throw new IllegalArgumentException();
			}
			if (isSet[WEEK_OF_YEAR]
				&& (fields[WEEK_OF_YEAR] < 1 || fields[WEEK_OF_YEAR] > 53)) {
				throw new IllegalArgumentException();
			}
			if (isSet[DAY_OF_WEEK]
				&& (fields[DAY_OF_WEEK] < 1 || fields[DAY_OF_WEEK] > 7)) {
				throw new IllegalArgumentException();
			}
			if (isSet[DAY_OF_WEEK_IN_MONTH]
				&& (fields[DAY_OF_WEEK_IN_MONTH] < 1 || fields[DAY_OF_WEEK_IN_MONTH] > 6)) {
				throw new IllegalArgumentException();
			}
			if (isSet[WEEK_OF_MONTH]
				&& (fields[WEEK_OF_MONTH] < 1 || fields[WEEK_OF_MONTH] > 6)) {
				throw new IllegalArgumentException();
			}
			if (isSet[AM_PM] && fields[AM_PM] != AM && fields[AM_PM] != PM) {
				throw new IllegalArgumentException();
			}
			if (isSet[HOUR] && (fields[HOUR] < 0 || fields[HOUR] > 11)) {
				throw new IllegalArgumentException();
			}
			if (isSet[YEAR]) {
				if (isSet[ERA] && fields[ERA] == BC
					&& (fields[YEAR] < 1 || fields[YEAR] > 292269054)) {
					throw new IllegalArgumentException();
				} else if (fields[YEAR] < 1 || fields[YEAR] > 292278994) {
					throw new IllegalArgumentException();
				}
			}
			if (isSet[MONTH] && (fields[MONTH] < 0 || fields[MONTH] > 11)) {
				throw new IllegalArgumentException();
			}
		}

		long timeVal;
		long hour = 0;
		if (isSet[HOUR_OF_DAY] && lastTimeFieldSet != HOUR) {
			hour = fields[HOUR_OF_DAY];
		} else if (isSet[HOUR]) {
			hour = (fields[AM_PM] * 12) + fields[HOUR];
		}
		timeVal = hour * 3600000;

		if (isSet[MINUTE]) {
			timeVal += ((long) fields[MINUTE]) * 60000;
		}
		if (isSet[SECOND]) {
			timeVal += ((long) fields[SECOND]) * 1000;
		}
		if (isSet[MILLISECOND]) {
			timeVal += fields[MILLISECOND];
		}

		long days;
		int year = isSet[YEAR] ? fields[YEAR] : 1970;
		if (isSet[ERA]) {
			// Always test for valid ERA, even if the Calendar is lenient
			if (fields[ERA] != BC && fields[ERA] != AD) {
				throw new IllegalArgumentException();
			}
			if (fields[ERA] == BC) {
				year = 1 - year;
			}
		}

		boolean weekMonthSet = isSet[WEEK_OF_MONTH]
			|| isSet[DAY_OF_WEEK_IN_MONTH];
		boolean useMonth = (isSet[DATE] || isSet[MONTH] || weekMonthSet)
			&& lastDateFieldSet != DAY_OF_YEAR;
		if (useMonth
			&& (lastDateFieldSet == DAY_OF_WEEK || lastDateFieldSet == WEEK_OF_YEAR)) {
			if (isSet[WEEK_OF_YEAR] && isSet[DAY_OF_WEEK]) {
				useMonth = lastDateFieldSet != WEEK_OF_YEAR && weekMonthSet
					&& isSet[DAY_OF_WEEK];
			} else if (isSet[DAY_OF_YEAR]) {
				useMonth = isSet[DATE] && isSet[MONTH];
			}
		}

		if (useMonth) {
			int month = fields[MONTH];
			year += month / 12;
			month %= 12;
			if (month < 0) {
				year--;
				month += 12;
			}
			boolean leapYear = isLeapYear(year);
			days = daysFromBaseYear(year) + daysInYear(leapYear, month);
			boolean useDate = isSet[DATE];
			if (useDate
				&& (lastDateFieldSet == DAY_OF_WEEK
				|| lastDateFieldSet == WEEK_OF_MONTH || lastDateFieldSet == DAY_OF_WEEK_IN_MONTH)) {
				useDate = !(isSet[DAY_OF_WEEK] && weekMonthSet);
			}
			if (useDate) {
				if (!isLenient()
					&& (fields[DATE] < 1 || fields[DATE] > daysInMonth(
					leapYear, month))) {
					throw new IllegalArgumentException();
				}
				days += fields[DATE] - 1;
			} else {
				int dayOfWeek;
				if (isSet[DAY_OF_WEEK]) {
					dayOfWeek = fields[DAY_OF_WEEK] - 1;
				} else {
					dayOfWeek = getFirstDayOfWeek() - 1;
				}
				if (isSet[WEEK_OF_MONTH]
					&& lastDateFieldSet != DAY_OF_WEEK_IN_MONTH) {
					int skew = mod7(days - 3 - (getFirstDayOfWeek() - 1));
					days += (fields[WEEK_OF_MONTH] - 1) * 7
						+ mod7(skew + dayOfWeek - (days - 3)) - skew;
				} else if (isSet[DAY_OF_WEEK_IN_MONTH]) {
					if (fields[DAY_OF_WEEK_IN_MONTH] >= 0) {
						days += mod7(dayOfWeek - (days - 3))
							+ (fields[DAY_OF_WEEK_IN_MONTH] - 1) * 7;
					} else {
						days += daysInMonth(leapYear, month)
							+ mod7(dayOfWeek
							- (days + daysInMonth(leapYear, month) - 3))
							+ fields[DAY_OF_WEEK_IN_MONTH] * 7;
					}
				} else if (isSet[DAY_OF_WEEK]) {
					int skew = mod7(days - 3 - (getFirstDayOfWeek() - 1));
					days += mod7(mod7(skew + dayOfWeek - (days - 3)) - skew);
				}
			}
		} else {
			boolean useWeekYear = isSet[WEEK_OF_YEAR]
				&& lastDateFieldSet != DAY_OF_YEAR;
			if (useWeekYear && isSet[DAY_OF_YEAR]) {
				useWeekYear = isSet[DAY_OF_WEEK];
			}
			days = daysFromBaseYear(year);
			if (useWeekYear) {
				int dayOfWeek;
				if (isSet[DAY_OF_WEEK]) {
					dayOfWeek = fields[DAY_OF_WEEK] - 1;
				} else {
					dayOfWeek = getFirstDayOfWeek() - 1;
				}
				int skew = mod7(days - 3 - (getFirstDayOfWeek() - 1));
				days += (fields[WEEK_OF_YEAR] - 1) * 7
					+ mod7(skew + dayOfWeek - (days - 3)) - skew;
				if (7 - skew < getMinimalDaysInFirstWeek()) {
					days += 7;
				}
			} else if (isSet[DAY_OF_YEAR]) {
				if (!isLenient()
					&& (fields[DAY_OF_YEAR] < 1 || fields[DAY_OF_YEAR] > (365 + (isLeapYear(year) ? 1
					: 0)))) {
					throw new IllegalArgumentException();
				}
				days += fields[DAY_OF_YEAR] - 1;
			} else if (isSet[DAY_OF_WEEK]) {
				days += mod7(fields[DAY_OF_WEEK] - 1 - (days - 3));
			}
		}
		lastDateFieldSet = 0;

		timeVal += days * 86400000;
		// Use local time to compare with the gregorian change
		if (year == changeYear
			&& timeVal >= gregorianCutover + julianError() * 86400000L) {
			timeVal -= julianError() * 86400000L;
		}

		// It is not possible to simply subtract getOffset(timeVal) from timeVal
		// to get UTC.
		// The trick is needed for the moment when DST transition occurs,
		// say 1:00 is a transition time when DST offset becomes +1 hour,
		// then wall time in the interval 1:00 - 2:00 is invalid and is
		// treated as UTC time.
		long timeValWithoutDST = timeVal - getOffset(timeVal)
			+ getTimeZone().getRawOffset();
		timeVal -= getOffset(timeValWithoutDST);
		// Need to update wall time in fields, since it was invalid due to DST
		// transition
		this.time = timeVal;
		if (timeValWithoutDST != timeVal) {
			computeFields();
			areFieldsSet = true;
		}
	}

	private int computeYearAndDay(long dayCount, long localTime) {
		int year = 1970;
		long days = dayCount;
		if (localTime < gregorianCutover) {
			days -= julianSkew;
		}
		int approxYears;

		while ((approxYears = (int) (days / 365)) != 0) {
			year = year + approxYears;
			days = dayCount - daysFromBaseYear(year);
		}
		if (days < 0) {
			year = year - 1;
			days = days + daysInYear(year);
		}
		fields[YEAR] = year;
		return (int) days + 1;
	}

	private long daysFromBaseYear(long year) {
		if (year >= 1970) {
			long days = (year - 1970) * 365 + ((year - 1969) / 4);
			if (year > changeYear) {
				days -= ((year - 1901) / 100) - ((year - 1601) / 400);
			} else {
				if (year == changeYear) {
					days += currentYearSkew;
				} else if (year == changeYear - 1) {
					days += lastYearSkew;
				} else {
					days += julianSkew;
				}
			}
			return days;
		} else if (year <= changeYear) {
			return (year - 1970) * 365 + ((year - 1972) / 4) + julianSkew;
		}
		return (year - 1970) * 365 + ((year - 1972) / 4) - ((year - 2000) / 100) + ((year - 2000) / 400);
	}

	private int daysInMonth() {
		return daysInMonth(isLeapYear(fields[YEAR]), fields[MONTH]);
	}

	private int daysInMonth(boolean leapYear, int month) {
		if (leapYear && month == FEBRUARY) {
			return DaysInMonth[month] + 1;
		}

		return DaysInMonth[month];
	}

	private int daysInYear(int year) {
		int daysInYear = isLeapYear(year) ? 366 : 365;
		if (year == changeYear) {
			daysInYear -= currentYearSkew;
		}
		if (year == changeYear - 1) {
			daysInYear -= lastYearSkew;
		}
		return daysInYear;
	}

	private int daysInYear(boolean leapYear, int month) {
		if (leapYear && month > FEBRUARY) {
			return DaysInYear[month] + 1;
		}

		return DaysInYear[month];
	}

	/**
	 * Returns true if {@code object} is a GregorianCalendar with the same
	 * properties.
	 */
	@Override
	public boolean equals(Object object) {
		if (!(object instanceof GregorianCalendar)) {
			return false;
		}
		if (object == this) {
			return true;
		}
		return super.equals(object)
			&& gregorianCutover == ((GregorianCalendar) object).gregorianCutover;
	}

	@Override
	public int getActualMaximum(int field) {
		int value;
		if ((value = maximums[field]) == leastMaximums[field]) {
			return value;
		}

		complete();
		long orgTime = time;
		int result = 0;
		switch (field) {
			case WEEK_OF_YEAR:
				set(DATE, 31);
				set(MONTH, DECEMBER);
				result = get(WEEK_OF_YEAR);
				if (result == 1) {
					set(DATE, 31 - 7);
					result = get(WEEK_OF_YEAR);
				}
				areFieldsSet = false;
				break;
			case WEEK_OF_MONTH:
				set(DATE, daysInMonth());
				result = get(WEEK_OF_MONTH);
				areFieldsSet = false;
				break;
			case DATE:
				return daysInMonth();
			case DAY_OF_YEAR:
				return daysInYear(fields[YEAR]);
			case DAY_OF_WEEK_IN_MONTH:
				result = get(DAY_OF_WEEK_IN_MONTH)
					+ ((daysInMonth() - get(DATE)) / 7);
				break;
			case YEAR:
				GregorianCalendar clone = (GregorianCalendar) clone();
				if (get(ERA) == AD) {
					clone.setTimeInMillis(Long.MAX_VALUE);
				} else {
					clone.setTimeInMillis(Long.MIN_VALUE);
				}
				result = clone.get(YEAR);
				clone.set(YEAR, get(YEAR));
				if (clone.before(this)) {
					result--;
				}
				break;
			case DST_OFFSET:
				result = getMaximum(DST_OFFSET);
				break;
		}
		time = orgTime;
		return result;
	}

	/**
	 * Gets the minimum value of the specified field for the current date. For
	 * the gregorian calendar, this value is the same as
	 * {@code getMinimum()}.
	 *
	 * @param field the field.
	 * @return the minimum value of the specified field.
	 */
	@Override
	public int getActualMinimum(int field) {
		return getMinimum(field);
	}

	/**
	 * Gets the greatest minimum value of the specified field. For the gregorian
	 * calendar, this value is the same as {@code getMinimum()}.
	 *
	 * @param field the field.
	 * @return the greatest minimum value of the specified field.
	 */
	@Override
	public int getGreatestMinimum(int field) {
		return minimums[field];
	}

	/**
	 * Returns the gregorian change date of this calendar. This is the date on
	 * which the gregorian calendar came into effect.
	 *
	 * @return a {@code Date} which represents the gregorian change date.
	 */
	public final Date getGregorianChange() {
		return new Date(gregorianCutover);
	}

	/**
	 * Gets the smallest maximum value of the specified field. For example, 28
	 * for the day of month field.
	 *
	 * @param field the field.
	 * @return the smallest maximum value of the specified field.
	 */
	@Override
	public int getLeastMaximum(int field) {
		// return value for WEEK_OF_YEAR should make corresponding changes when
		// the gregorian change date have been reset.
		if (gregorianCutover != defaultGregorianCutover
			&& field == WEEK_OF_YEAR) {
			long currentTimeInMillis = time;
			setTimeInMillis(gregorianCutover);
			int actual = getActualMaximum(field);
			setTimeInMillis(currentTimeInMillis);
			return actual;
		}
		return leastMaximums[field];
	}

	/**
	 * Gets the greatest maximum value of the specified field. For example, 31
	 * for the day of month field.
	 *
	 * @param field the field.
	 * @return the greatest maximum value of the specified field.
	 */
	@Override
	public int getMaximum(int field) {
		return maximums[field];
	}

	/**
	 * Gets the smallest minimum value of the specified field.
	 *
	 * @param field the field.
	 * @return the smallest minimum value of the specified field.
	 */
	@Override
	public int getMinimum(int field) {
		return minimums[field];
	}

	private int getOffset(long localTime) {
		TimeZone timeZone = getTimeZone();

		long dayCount = localTime / 86400000;
		int millis = (int) (localTime % 86400000);
		if (millis < 0) {
			millis += 86400000;
			dayCount--;
		}

		int year = 1970;
		long days = dayCount;
		if (localTime < gregorianCutover) {
			days -= julianSkew;
		}
		int approxYears;

		while ((approxYears = (int) (days / 365)) != 0) {
			year = year + approxYears;
			days = dayCount - daysFromBaseYear(year);
		}
		if (days < 0) {
			year = year - 1;
			days = days + 365 + (isLeapYear(year) ? 1 : 0);
			if (year == changeYear && localTime < gregorianCutover) {
				days -= julianError();
			}
		}
		if (year <= 0) {
			return timeZone.getRawOffset();
		}
		int dayOfYear = (int) days + 1;

		int month = dayOfYear / 32;
		boolean leapYear = isLeapYear(year);
		int date = dayOfYear - daysInYear(leapYear, month);
		if (date > daysInMonth(leapYear, month)) {
			date -= daysInMonth(leapYear, month);
			month++;
		}
		int dayOfWeek = mod7(dayCount - 3) + 1;
		return timeZone.getOffset(AD, year, month, date, dayOfWeek, millis);
	}

	@Override
	public int hashCode() {
		return super.hashCode()
			+ ((int) (gregorianCutover >>> 32) ^ (int) gregorianCutover);
	}

	/**
	 * Returns true if {@code year} is a leap year.
	 */
	public boolean isLeapYear(int year) {
		if (year > changeYear) {
			return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
		}

		return year % 4 == 0;
	}

	private int julianError() {
		return changeYear / 100 - changeYear / 400 - 2;
	}

	private int mod(int value, int mod) {
		int rem = value % mod;
		if (value < 0 && rem < 0) {
			return rem + mod;
		}
		return rem;
	}

	private int mod7(long num1) {
		int rem = (int) (num1 % 7);
		if (num1 < 0 && rem < 0) {
			return rem + 7;
		}
		return rem;
	}

	/**
	 * Adds the specified amount the specified field and wraps the value of the
	 * field when it goes beyond the maximum or minimum value for the current
	 * date. Other fields will be adjusted as required to maintain a consistent
	 * date.
	 *
	 * @param field the field to roll.
	 * @param value the amount to add.
	 * @throws IllegalArgumentException if an invalid field is specified.
	 */
	@Override
	public void roll(int field, int value) {
		if (value == 0) {
			return;
		}
		if (field < 0 || field >= ZONE_OFFSET) {
			throw new IllegalArgumentException();
		}

		complete();
		int days, day, mod, maxWeeks, newWeek;
		int max = -1;
		switch (field) {
			case YEAR:
				max = maximums[field];
				break;
			case WEEK_OF_YEAR:
				days = daysInYear(fields[YEAR]);
				day = DAY_OF_YEAR;
				mod = mod7(fields[DAY_OF_WEEK] - fields[day]
					- (getFirstDayOfWeek() - 1));
				maxWeeks = (days - 1 + mod) / 7 + 1;
				newWeek = mod(fields[field] - 1 + value, maxWeeks) + 1;
				if (newWeek == maxWeeks) {
					int addDays = (newWeek - fields[field]) * 7;
					if (fields[day] > addDays && fields[day] + addDays > days) {
						set(field, 1);
					} else {
						set(field, newWeek - 1);
					}
				} else if (newWeek == 1) {
					int week = (fields[day] - ((fields[day] - 1) / 7 * 7) - 1 + mod) / 7 + 1;
					if (week > 1) {
						set(field, 1);
					} else {
						set(field, newWeek);
					}
				} else {
					set(field, newWeek);
				}
				break;
			case WEEK_OF_MONTH:
				days = daysInMonth();
				day = DATE;
				mod = mod7(fields[DAY_OF_WEEK] - fields[day]
					- (getFirstDayOfWeek() - 1));
				maxWeeks = (days - 1 + mod) / 7 + 1;
				newWeek = mod(fields[field] - 1 + value, maxWeeks) + 1;
				if (newWeek == maxWeeks) {
					if (fields[day] + (newWeek - fields[field]) * 7 > days) {
						set(day, days);
					} else {
						set(field, newWeek);
					}
				} else if (newWeek == 1) {
					int week = (fields[day] - ((fields[day] - 1) / 7 * 7) - 1 + mod) / 7 + 1;
					if (week > 1) {
						set(day, 1);
					} else {
						set(field, newWeek);
					}
				} else {
					set(field, newWeek);
				}
				break;
			case DATE:
				max = daysInMonth();
				break;
			case DAY_OF_YEAR:
				max = daysInYear(fields[YEAR]);
				break;
			case DAY_OF_WEEK:
				max = maximums[field];
				lastDateFieldSet = WEEK_OF_MONTH;
				break;
			case DAY_OF_WEEK_IN_MONTH:
				max = (fields[DATE] + ((daysInMonth() - fields[DATE]) / 7 * 7) - 1) / 7 + 1;
				break;

			case ERA:
			case MONTH:
			case AM_PM:
			case HOUR:
			case HOUR_OF_DAY:
			case MINUTE:
			case SECOND:
			case MILLISECOND:
				set(field, mod(fields[field] + value, maximums[field] + 1));
				if (field == MONTH && fields[DATE] > daysInMonth()) {
					set(DATE, daysInMonth());
				} else if (field == AM_PM) {
					lastTimeFieldSet = HOUR;
				}
				break;
		}
		if (max != -1) {
			set(field, mod(fields[field] - 1 + value, max) + 1);
		}
		complete();
	}

	/**
	 * Increments or decrements the specified field and wraps the value of the
	 * field when it goes beyond the maximum or minimum value for the current
	 * date. Other fields will be adjusted as required to maintain a consistent
	 * date. For example, March 31 will roll to April 30 when rolling the month
	 * field.
	 *
	 * @param field     the field to roll.
	 * @param increment {@code true} to increment the field, {@code false} to
	 *                  decrement.
	 * @throws IllegalArgumentException if an invalid field is specified.
	 */
	@Override
	public void roll(int field, boolean increment) {
		roll(field, increment ? 1 : -1);
	}

	/**
	 * Sets the gregorian change date of this calendar.
	 */
	public void setGregorianChange(Date date) {
		gregorianCutover = date.getTime();
		GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
		cal.setTime(date);
		changeYear = cal.get(YEAR);
		if (cal.get(ERA) == BC) {
			changeYear = 1 - changeYear;
		}
		julianSkew = ((changeYear - 2000) / 400) + julianError()
			- ((changeYear - 2000) / 100);
		int dayOfYear = cal.get(DAY_OF_YEAR);
		if (dayOfYear < julianSkew) {
			currentYearSkew = dayOfYear - 1;
			lastYearSkew = julianSkew - dayOfYear + 1;
		} else {
			lastYearSkew = 0;
			currentYearSkew = julianSkew;
		}
	}

	private void writeObject(ObjectOutputStream stream) throws IOException {
		stream.defaultWriteObject();
	}

	private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
		stream.defaultReadObject();
		setGregorianChange(new Date(gregorianCutover));
	}
}
